Vue 双向数据绑定

Object.defineProperty

Vue 在3.0版本之前是利用了 JavaScript ES5 提供的元编程接口 Object.defineProperty进行数据劫持,从而将数据变成了「响应式」的。

基于「响应式数据」实现了MVVM 数据双向绑定,所谓MVVM 数据双向绑定主要是指:数据变化更新视图,视图变化更新数据。

Object.defineProperty 的作用

Vue 2.X 的数据双向绑定都是依据 Object.defineProperty()这一方法来做的

Object.defineProperty(obj, prop, descriptor)

Object.defineProperty 方法接收三个参数:

  • obj

    要定义属性的对象。

  • prop

    要定义或修改的属性的名称或 Symbol

  • descriptor

    要定义或修改的属性描述符。

返回值:

被传递给函数的对象。


通过这个方法我们可以进行,JavaScript 语言级别的编程(即:对 JavaScript 语言进行编程,使其一些原生的操作可以按我们的逻辑进行执行)。

简单的说就是可以用这个方法,对一个对象的某个属性的描述符进行定义。

Vue 进行数据劫持将数据变为响应式的就使用到了描述符当中的 gettersetter

  • get: 当查找某个对象属性时,该对象属性将会与被调用函数绑定。

  • set: 当试图设置该属性时,对象属性与被调用函数绑定。

const obj = {
  name: 'playlife',
  age: 23
}
obj.age //23
obj.age = 18

像代码中的两个操作:读取和赋值,就是在访问 obj.agegettersetter

Object.defineProperty(obj, 'age', {
  get: function () {
    return 18 //永远18岁
  },
  set: function (newValue) {
    console.log('不让你设置', newValue)
  }
})
console.log(obj.age)//18
obj.age = 1//不让你设置 1

gettersetter 都是方法函数,所以可以这么写

Object.defineProperty(obj, 'age', {
  get() {
    return 18
  },
  set(newValue) {
    console.log('不让你设置', newValue)
  }
})

Vue 的「双向数据绑定」就是根据上面的原理来实现的。

只要在读取值时收集「观察者」在赋值时触发「观察者的更新函数」就可以实现数据变更,从而实现 DOM 重新渲染。

Vue 实现双向数据绑定

在Vue主线里和数据双向绑定有关的有以下几个模块

  • Vue 构造函数 new Vue()
  • 观察者 observer
  • 观察者 watcher
  • 指令系统 directive类directives指令函数集合
  • DOM 解析 compile
  • watcherobserver 之间的联系者 dep

vue-mvvm

结合以上 Vue 源码各模块函数关系图来复述一个 Vue的实现过程:

实例化之前

在引入Vue文件时 Vue本身会有几个类函数和一个处理函数集合

  • observer
  • watcher
  • dep
  • directive
  • directives

实例化过程

首先会调用 initData() initMethods()等一系列方法,将数据挂载到 Vue实例上。这样就可以通过 vm.xxx 或者 vm.$xxx直接读取数据和调用函数。

接下来会调用observe(data)对数据进行监听。其实就是使用Object.defineProperty()方法,对每一个 key 都建立一个 dep实例。

并且在 gettersetter作了一些设置,当访问这一个 keygetter就会触发 getter 函数里的 dep.depend方法收集依赖 (watcher实例)。

当对这一个 key 赋值时:就会触发setter 里的 dep.notify方法,通知 dep 收集的所有 watcher实例调用update方法进行更新。dep 有一个 watcher实例数组,触发更新遍历这个数组,执行watcher.update()方法。

上面完成之后就会调用compile函数开始对 DOM 进行解析了。首先会解析节点,然后再解析节点里的{{插值表达式}}v-if等指令。

解析{{插值表达式}} 时会生成一个 text 指令。并把{{插值表达式}} 替换为一个空的文本节点,然后生成一个描述符对象。

描述符对象收集了后面生成指令实例时所需要的数据、要监听的表达式、对应的文本节点,和指令对应的处理函数。

然后会将这个描述符当作参数传入directive 类,生成一个指令实例。指令实例执行bind方法,bind方法会将表达式、指令处理函数以及相关的一些参数传给 watcher生成一个watcher实例。

watcher 首次会执行 get 方法对表达式进行求值,然后将得到的值将给 update方法。

update 方法将值传入处理函数对 DOM 进行更新,这样就完成了第一次渲染。

实例化完成后

以后每一次更改的数据值,都会触发数据所对应对象属性这个 keysetter 方法。

setter 方法再触发 dep.notify 通知对应的 watcher 调用update方法进行更新。

update方法再把值传给对应的处理函数,再一次进行 DOM 渲染,如此循环往复。

其他的指令也是按照这一流程来运行的!

data

此时,看着这张图就不会再一脸懵逼了!

Object.defineProperty 的缺陷

Vue3.0 采用了 Proxy,抛弃了Object.defineProperty?

  1. Object.defineProperty无法监控到数组下标的变化,导致通过数组下标添加元素,不能实时响应;
  2. Object.defineProperty只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果,属性值是对象,还需要深度遍历。Proxy可以劫持整个对象,并返回一个新的对象。
  3. Proxy不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。

对于数组

Object.defineProperty 无法监控到数组下标的变化,导致直接通过数组的下标给数组设置值,不能实时响应。 为了解决这个问题,经过 Vue 内部处理后可以使用以下几种方法来监听数组:

push()
pop()
shift()
unshift()
splice()
sort()
reverse()

由于只针对了以上八种方法进行了hack处理,所以其他数组的属性也是检测不到的,还是具有一定的局限性。

对于对象

Object.defineProperty 还无法监控到对象属性的添加或移除。由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的。

对于已经创建的实例,Vue 不允许动态添加根级别的响应式 property。但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式 property。

还可以使用 vm.$set 实例方法,这也是全局 Vue.set 方法的别名

Proxy 实现的双向绑定的特点

语法:

const p = new Proxy(target, handler)
  • handler:包含捕捉器(trap)的占位符对象,可译为处理器对象。
  • target:被 Proxy 代理的对象。

Proxy 的优点:

Proxy 可以直接监听对象而非属性

Proxy 可以直接监听数组的变化

Proxy 有多达13种拦截方法,不限于apply、ownKeys、deleteProperty、has等等是Object.defineProperty不具备的。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!